package common.sql;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.SoftReference;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import common.db.jdbc.Jdbc;

import core.exception.PhysicalException;
import core.exception.ThrowableUtil;
import core.util.MapUtil;

/**
 * クエリユーティリティ
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class QueryUtil {
	/** ログ出力クラス */
	private static final Logger LOGGER = LogManager.getLogger(QueryUtil.class);

	/** クエリ格納用ハッシュ */
	private static final AtomicReference<SoftReference<ConcurrentMap<String, LineParsedList>>>
	QUERIES = new AtomicReference<>(new SoftReference<ConcurrentMap<String, LineParsedList>>(null));

	/**
	 * コンストラクタ
	 */
	private QueryUtil() {
		throw new AssertionError();
	}

	/**
	 * LineParsedQuery取得
	 * @param qry クエリ
	 * @return LineParsedQuery
	 */
	public static LineParsedList getLineParsedQuery(final String qry) {
		ConcurrentMap<String, LineParsedList> map = MapUtil.getCacheMap(QUERIES);
		LineParsedList pq = map.get(qry);
		if (pq == null) {
			pq = new LineParsedList(qry);
			if (map.putIfAbsent(qry, pq) != null) {
				pq = map.get(qry);
			}
		}
		return pq;
	}

	/**
	 * クエリファイル変換
	 * @param val クエリ名（.セパレイト）
	 * @return クエリファイル(.sql)付き
	 */
	private static String toQueryFile(final String val) {
		final String sql = ".sql";
		String str = val;
		if (str != null) {
			if (str.toLowerCase(Locale.ENGLISH).endsWith(sql)) {
				str = str.substring(0, str.length() - sql.length()).replace(".", "/") + sql;
			} else {
				str = str.replace(".", "/") + sql;
			}
		}
		return str;
	}

	/**
	 * ファイルからSQL取得
	 * @param name ファイル名（パッケージ付き）
	 * @return SQL
	 */
	public static String getSqlFromFile(final String name) {
		String file = toQueryFile(name);
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		try (InputStream is = cl.getResourceAsStream(file)) {
			try (InputStreamReader isr = new InputStreamReader(is, getCharset(file))) {
				try (BufferedReader br = new BufferedReader(isr)) {
					StringBuilder sb = new StringBuilder();
					for (String str = br.readLine(); str != null; str = br.readLine()) {
						sb.append(str).append("\n");
					}
					return sb.toString();
				}
			}
		} catch (IOException ex) {
			LogManager.getLogger().error(ex.getMessage(), ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * ステートメント作成
	 * @param conn コネクション
	 * @param qry クエリ
	 * @param param パラメタマップ
	 * @return ステートメント
	 * @throws SQLException SQL例外
	 */
	public static PreparedStatement createStatement(final Connection conn,
			final String qry, final Map<String, ?> param) throws SQLException {
		// SQL文作成
		PreparedStatement psmt = null;
		try {
			if (!Objects.toString(qry, "").isEmpty()) {
				List<Object> qlist = new ArrayList<>();

				String query = toPrepareQuery(qry, param, qlist);
				psmt = Jdbc.wrap(conn).readonlyStatement(query);

				// パラメタ設定
				int i = 1;
				for (final Object obj : qlist) {
					psmt.setObject(i++, obj);
				}

				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("QUERY=" + toCompleteSql(query, qlist));
				}
			}
			return psmt;
		} catch (SQLException ex) {
			if (psmt != null) {
				try {
					psmt.close();
				} catch (SQLException e) {
					ThrowableUtil.warn(e);
				}
			}
			throw ex;
		}
	}

	/**
	 * PrepareSQL変換
	 *
	 * @param qry 元クエリ
	 * @param map パラメタマップ
	 * @param list パラメタリスト（出力）
	 * @return PrepareSQL
	 */
	public static String toPrepareQuery(final String qry,
			final Map<String, ?> map, final List<Object> list) {
		Map<String, ?> pmap = map;
		if (pmap == null) {
			pmap = Collections.emptyMap();
		}
		return getLineParsedQuery(qry).build(pmap, list);
	}

	/**
	 * パラメタ埋込済SQL取得
	 *
	 * @param query 埋込前文字列
	 * @param params パラメタ
	 * @return 埋込済み文字列
	 */
	private static String toCompleteSql(final String query, final List<?> params) {
		StringBuilder sb = new StringBuilder();
		if (query != null) {
			for (int i = 0, s = 0, e = query.indexOf('?'); s < query.length();
					s = e + "?".length(), e = query.indexOf('?', s), i++) {
				if (e < 0) {
					sb.append(query.substring(s, query.length()));
					break;
				}

				sb.append(query.substring(s, e));
				if (params != null && i < params.size()) {
					if (!Number.class.isInstance(params.get(i))) {
						sb.append("'");
						sb.append(params.get(i));
						sb.append("'");
					} else {
						sb.append(params.get(i));
					}
				}
			}
		}
		return sb.toString();
	}

	/**
	 * エンコード名取得
	 * @param name リソース名
	 * @return エンコード名
	 */
	private static Charset getCharset(final String name) {
		final String encoding = "--encoding:";
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		try (InputStream is = cl.getResourceAsStream(name)) {
			byte[] bytes = new byte[128];
			if (is != null && 0 < is.read(bytes)) {
				String str = new String(bytes, StandardCharsets.ISO_8859_1);
				if (str.startsWith(encoding)) {
					int end = str.indexOf('\n');
					if (0 < end) {
						int e = str.offsetByCodePoints(end, -1);
						if (str.codePointAt(e) == '\r') {
							end = e;
						}
					}
					return Charset.forName(str.substring(encoding.length(), end));
				}
			}
			return StandardCharsets.UTF_8;

		} catch (IOException ex) {
			LogManager.getLogger().error(ex.getMessage(), ex);
			throw new PhysicalException(ex);
		}
	}
}
